package nl.boektrust.btsoep.server;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import nl.boektrust.btsoep.client.PersistentService;
import nl.boektrust.btsoep.shared.model.BTEntity;
import nl.boektrust.btsoep.shared.model.BinaryAsset;
import nl.boektrust.btsoep.shared.model.Book;
import nl.boektrust.btsoep.shared.model.BookManifestation;
import nl.boektrust.btsoep.shared.model.ContentSpecifier;
import nl.boektrust.btsoep.shared.model.ContentSpecifierType;
import nl.boektrust.btsoep.shared.model.Contributor;
import nl.boektrust.btsoep.shared.model.ImageReference;
import nl.boektrust.btsoep.shared.model.ImageReferenceTypeCode;
import nl.boektrust.btsoep.shared.model.Person;
import nl.boektrust.btsoep.shared.model.PhysicalBookManifestation;
import nl.boektrust.btsoep.shared.model.StopWords;

import com.allen_sauer.gwt.log.client.Log;
import com.google.appengine.api.blobstore.BlobstoreService;
import com.google.appengine.api.blobstore.BlobstoreServiceFactory;
import com.google.appengine.api.datastore.DatastoreTimeoutException;
import com.google.appengine.api.images.ImagesService;
import com.google.appengine.api.images.ImagesServiceFactory;
import com.google.gwt.core.client.GWT;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.Objectify;
import com.googlecode.objectify.ObjectifyService;
import com.googlecode.objectify.impl.QueryImpl;
import com.googlecode.objectify.util.QueryWrapper;

@SuppressWarnings("serial")
public class PersistentServiceImpl extends RemoteServiceServlet implements PersistentService {
	
	private static boolean isObjectifyServiceRegistered = false;
	public ImagesService imagesService = (ImagesService) ImagesServiceFactory.getImagesService();;
	private static Objectify ob = null;
	//Method must be present in Service Interface and Async Interface
	
	static {
		if (isObjectifyServiceRegistered == false) {
			System.out.println("Server: statically registering services");
			ObjectifyService.register(BinaryAsset.class);
			ObjectifyService.register(BTEntity.class);
			ObjectifyService.register(Person.class);
			ObjectifyService.register(PhysicalBookManifestation.class);
			ObjectifyService.register(BookManifestation.class);
			ObjectifyService.register(Book.class);
		}
		isObjectifyServiceRegistered = true;
		if (ob == null) ob = ObjectifyService.begin();
	}

	
	public void registerServices () {
		if (isObjectifyServiceRegistered == false) {
	    	System.out.println("Server: registerServices: entering registerServices()");
			ObjectifyService.register(BinaryAsset.class);
			ObjectifyService.register(BTEntity.class);
			ObjectifyService.register(Person.class);
			ObjectifyService.register(PhysicalBookManifestation.class);
			ObjectifyService.register(BookManifestation.class);
			ObjectifyService.register(Book.class);
			isObjectifyServiceRegistered = true;
		}
		if (ob == null) ob = ObjectifyService.begin();

	}
	
    public void persistBTEntity(String name, String url) {
    	System.err.println("Server: entering persistBTEntity(name: "+name+", url: "+url+")");
    	registerServices();
		Objectify ob = ObjectifyService.begin();
		BTEntity e = new BTEntity(name, url);
		ob.put(e);
    }
    
    public void persistBinaryAsset(BinaryAsset b) {
    	//System.out.println("Server: entering persistBTEntity(name: "+name+", url: "+url+")");
    	//	registerServices();
		Objectify ob = ObjectifyService.begin();
		ob.put(b);
    }
    
    public void persistPhysicalBookManifestation(PhysicalBookManifestation b) {
    	boolean verbose_output = false;
    	if (verbose_output == true) System.out.println("Server: entering persistBook(title: "+b.getTitle()+", rec ref: "+b.getRecordReference()+")");
    	if (verbose_output == true) System.out.println("Server: entering persistBook(id: "+b.getId());
    	registerServices();
    	for (Contributor c : b.getContributors()) {
    		if (verbose_output == true) System.out.println("Server:persistPBM: adding contributor");

    		if (c.getPerson() != null) {
    			if (verbose_output == true) System.out.println("Server:persistPBM: person is non-empty");

    			if (c.getPerson().getId() == null) {
    				if (verbose_output == true) System.out.println("Server:persistPBM: person has a null id");
    				Key<Person> key = (Key<Person>)persistPerson(c.getPerson());
    				c.getPerson().setId(key.getId());
    				if (verbose_output == true) System.out.println("Server:persistPBM: person has now been assigned id: "+c.getPerson().getId());

    			} else {
    				if (verbose_output == true) System.out.println("Server:persistPBM: person already has an id, no DB action required.");
    			}
    			c.setPersonKey(new Key<Person>(Person.class, c.getPerson().getId()));
    		} else {
    			if (verbose_output == true) System.out.println("Server:persistPBM: person is empty");
    			if (verbose_output == true) System.err.println("persistPhysicalBookManifestation(): error: tried to persist a contributor which has a null Person. Skipped this Contributor, record may have been corrupted. RecordReference: "+b.getRecordReference());
    		}
    	}
    	Objectify ob = ObjectifyService.begin();
    	ob.put(b);
    }
    
    public Key<Person> persistPerson(Person p) {
    	registerServices();
    	if (p != null && p.getName() != null) System.err.println("Persist person called on person with name "+p.getName());
		Objectify ob = ObjectifyService.begin();
		return ob.put(p);
		
    }
    
    public void deletePerson(Person p) {
    	registerServices();
		Objectify ob = ObjectifyService.begin();
		ob.delete(p);
    	
    }
    
    
    public void deletePhysicalBookManifestation(PhysicalBookManifestation p) {
    	registerServices();
		Objectify ob = ObjectifyService.begin();
		ob.delete(p);
    	
    }
    
    public void deletePhysicalBookManifestation(Key<PhysicalBookManifestation> p) {
    	registerServices();
		Objectify ob = ObjectifyService.begin();
		ob.delete(p);
    	
    }

    
    public void updatePhysicalBookManifestationChunk(int chunk) {
    	registerServices();
    	System.err.println("EXPENSIVE METHOD ALERT: updatePhysicalBookManifestationChunk");

    	System.err.println("updatePhysChunk "+chunk);
    	Objectify ofy = ObjectifyService.begin();
    	Iterable<Key<PhysicalBookManifestation>> allKeys = ofy.query(PhysicalBookManifestation.class).offset(500*chunk).limit(500).fetchKeys();
    	for (Key<PhysicalBookManifestation> k : allKeys) {
    		while (true) {
    			try {
    				PhysicalBookManifestation b = (PhysicalBookManifestation)ofy.get(k);
    				for (Contributor c : b.getContributors()) {
    					if (c.getPersonKey() != null) {
    						try {
    							Person p = ofy.get(c.getPersonKey());
    							if (p != null) {
    								c.setPerson(p);
    							}
    						} catch (Exception e) {
    							e.printStackTrace();
    						}
    					}
    				}
    				//System.err.println("Updating book with recordreference "+b.getRecordReference());
    				deletePhysicalBookManifestation(b);
    				persistPhysicalBookManifestation(b);
    				//System.err.println("Updated book with recordreference "+b.getRecordReference());
    				break;
    			} catch (DatastoreTimeoutException e) {
    				System.err.println("Sleeping for timeout ");
    				int timeout_ms = 100;
    				try {
    					Thread.currentThread().sleep(timeout_ms);
    				} catch (InterruptedException e1) {
    					// TODO Auto-generated catch block
    					e1.printStackTrace();
    				}
    				timeout_ms *= 2;
    			} catch (Exception e) {
    				// Display a generic 500 Server Error page
    			} finally {
    				// Code that should be run regardless of whether the request succeeds,
    				// e.g. closing the PersistenceManager
    			}
    		}
    		//ofy.get(k);
    	}
    	System.err.println("done processing chunk "+chunk);
    }
    
    @SuppressWarnings("static-access")
	public int numPhysicalBookManifestations() {
    	registerServices();
    	System.err.println("EXPENSIVE METHOD ALERT: numPhysicalBookManifestations");

    	Objectify ofy = ObjectifyService.begin();
    	System.err.println("key lookup start");
    	Iterable<Key<PhysicalBookManifestation>> allKeys = ofy.query(PhysicalBookManifestation.class).fetchKeys();
    	System.err.println("key lookup done");
    	int numResults = 0;
    	for (Key<PhysicalBookManifestation> k : allKeys) {
    		numResults++;
    	} 
    	return numResults;
    }
    
    public int getNumberOfAssets(ImageReferenceTypeCode imageReferenceType) {
    	int numberOfAssets = ob.query(BinaryAsset.class).filter("imageReferenceTypeCode =", imageReferenceType).count();
    	return numberOfAssets;
    }
    
    public int getNumberOfEntities() {
    	Objectify ofy = ObjectifyService.begin();
    	return ofy.query(BTEntity.class).count();
    }
    
    public int getNumberOfPhysicalBooks() {
    	Objectify ofy = ObjectifyService.begin();
    	return ofy.query(PhysicalBookManifestation.class).count();
    }

    
    public List<BinaryAsset> getBinaryAssets(long recordReference, ImageReferenceTypeCode imageReferenceType) {
    	registerServices();
    	Objectify ofy = ObjectifyService.begin();

    	List<BinaryAsset> returnList = new ArrayList<BinaryAsset>();
    	returnList = (List<BinaryAsset>)ofy.query(BinaryAsset.class).filter("recordReference =", recordReference).filter("imageReferenceTypeCode =", imageReferenceType).list();
    	System.err.println("PersistentServiceImpl:getBinaryAssets(): found "+returnList.size()+" assets for recordreference "+recordReference);

    	return returnList;
    }
    
    public String getUploadURL(String path) {
    	BlobstoreService blobstoreService = BlobstoreServiceFactory.getBlobstoreService();
    	System.out.println("PersistentServiceImpl: getting upload URL for path "+path);
	    String url = blobstoreService.createUploadUrl(path);
    	System.out.println("PersistentServiceImpl: created upload URL "+url);

	    return url;
    }
    
    private ArrayList<String> getUrlsForImageReference(long recordreference, ImageReferenceTypeCode imageReferenceTypeCode) {
		String urlString = null;
		ArrayList<String> returnList = new ArrayList<String>();
		List<BinaryAsset> assets = getBinaryAssets(recordreference, imageReferenceTypeCode);
		if (assets != null) {
			for (BinaryAsset binarAysset: assets) {
				if (binarAysset.getBlobKey() != null) {
					urlString = imagesService.getServingUrl(binarAysset.getBlobKey());
					System.out.println("PersistentServiceImpl:getUrlsForImageReference: found URl for binary asset: "+urlString);
					returnList.add(urlString);

				}

			}
		}
		return returnList;		
	}
    
    public void deleteBinaryAssets(long recordReference, ImageReferenceTypeCode imageReferenceType) {
    	registerServices();
    	Objectify ofy = ObjectifyService.begin();

    	List<BinaryAsset> returnList = new ArrayList<BinaryAsset>();
    	returnList = getBinaryAssets(recordReference, imageReferenceType);
    	for (BinaryAsset b: returnList) {
    		ofy.delete(b);
    	}
    }
    
    
    public List<PhysicalBookManifestation> getAllPhysicalBookManifestations() {
    	registerServices();

    	System.err.println("EXPENSIVE METHOD ALERT: getAllPhysicalBookManifestations");

    	Objectify ofy = ObjectifyService.begin();
    	System.err.println("key lookup start");
    	Iterable<Key<PhysicalBookManifestation>> allKeys = ofy.query(PhysicalBookManifestation.class).fetchKeys();
    	System.err.println("key lookup done");
    	

    	List<PhysicalBookManifestation> bookmfList = new ArrayList<PhysicalBookManifestation>();
    	System.err.println("the actual query");
    	ofy.query(PhysicalBookManifestation.class).iterator();
    	System.err.println("the actual done");
    /*	// retrieve all contributors
    	for (PhysicalBookManifestation b : bookmfList) {
    		for (Contributor c : b.getContributors()) {
    			if (c.getPersonKey() != null) {
    				try {
    					Person p = ofy.get(c.getPersonKey());
    					if (p != null) {
    						c.setPerson(p);
    					}
    				} catch (Exception e) {
    					e.printStackTrace();
    				}
    			}
    		}
    	}
*/
    	return bookmfList;
    }
    
    
    
    public List<Person> getAllPersons() {
    	registerServices();
    	System.err.println("EXPENSIVE METHOD ALERT: getAllPersons");


    	Objectify ofy = ObjectifyService.begin();

    	List<Person> personList = new ArrayList<Person>();
    	personList = (List<Person>)ofy.query(Person.class).limit(200).list();
    	return personList;
    }
    
    public List<PhysicalBookManifestation> getBooksForPerson(Person p) {
    	registerServices();
    	Objectify ofy = ObjectifyService.begin();
    	List<PhysicalBookManifestation> pbm = ofy.query(PhysicalBookManifestation.class).filter("contributors.personKey", p).list();
    	return pbm;


    }
    
    public Person getPersonByFullName(String name) {
    	registerServices();
    	Objectify ofy = ObjectifyService.begin();
    	Person person = ofy.query(Person.class).filter("name", name).get();
    	return person;
    }
    
    
    public Person getPersonByPartialName(String name) {
    	registerServices();
    	Objectify ofy = ObjectifyService.begin();
    	Person person = ofy.query(Person.class).filter("parsedName", name).get();
    	return person;
    }
    
    public Person getPersonByKey(Key<Person> personKey) {
    	registerServices();
    	if (personKey == null) {
    		System.err.println("getPersonByKey: personKey is null");
    		return null;
    	}else {
    		System.err.println("getPersonByKey: personKey is not null");
    	}
    	Objectify ofy = ObjectifyService.begin();
    	Person person = ofy.get(personKey);
    	return person;
    }
    
    public List<Person> getPersonsByPartialName(String name) {
    	registerServices();
    	Objectify ofy = ObjectifyService.begin();
       	String start = name.toLowerCase();
    	System.out.println("finding persons with partial name: "+name);
    	List<Person> personList = (List<Person>)ofy.query(Person.class).filter("parsedName >=", start).filter("parsedName <", start + "\uFFFD").limit(200).list();
    	System.out.println("found "+personList.size()+" persons.");
    	return personList;
    }

    public PhysicalBookManifestation getBookByRecordReference(long recordReference) {
    	registerServices();
    	Objectify ofy = ObjectifyService.begin();	
    	PhysicalBookManifestation pbm = ofy.query(PhysicalBookManifestation.class).filter("recordReference", recordReference).get();
    	return pbm;
    }
    
    public static<T extends Comparable<? super T>> List<T> asSortedList(Collection<T> c) {
      List<T> list = new ArrayList<T>(c);
      java.util.Collections.sort(list);
      return list;
    }
    
    public List<PhysicalBookManifestation> getBooksByAuthor(String authorName) {
    	boolean generateWarnings = false;
    	HashSet<Long> pbmSet1 = new HashSet<Long>();
    	HashSet<Long> pbmSet2 = new HashSet<Long>();
    	HashMap<PhysicalBookManifestation, Integer> resultMap = new HashMap<PhysicalBookManifestation, Integer>();
     	
    	/*
    	 * retrieve partial title matches: parse title into words (ArrayList titleWords), remove stopwords
    	 * and then look up each word.
    	 */
    	List<String> authorWords = new ArrayList<String>();
    	String[] result = authorName.toLowerCase().split("[,\\.\\s]");
    	for (int x=0; x<result.length; x++) {
    		if (StopWords.isANameStopWord(result[x]) == false && result[x].length()>1) authorWords.add(result[x]);
    	}
    	
    	for (String authorWord : authorWords) {
    		List<PhysicalBookManifestation> booksByWordList = getBooksByNameWord(authorWord);
    		if (generateWarnings) System.err.println("booksByWordList (author name "+authorWord+") has "+booksByWordList.size()+" items");
    		/* put everything in a set to ensure uniqueness*/
        	pbmSet2 = new HashSet<Long>();
        	for (PhysicalBookManifestation book : booksByWordList) {
        		/*
        		 * check for uniqueness in the result set of the last query
        		 */
        		if (pbmSet2.add(book.getRecordReference()) != false) {
        			/*
        			 * check for uniqueness in the final result set.  If this book can't be added to pbmSet1, 
        			 * we know it's already in that set. So retrieve/delete/store it with increased sort priority. 
        			 */
        			if (pbmSet1.add(book.getRecordReference()) == false) {
        				/*
        				 * since this book could not be added to pbmSet1, we know it's already in that set. So retrieve 
        				 * it and increase the sort priority. 
        				 */
        				Set<PhysicalBookManifestation> keys = resultMap.keySet();
        				for (PhysicalBookManifestation b2: keys) {
        					if (b2.getRecordReference() == book.getRecordReference()) {
                				Integer value = (Integer)resultMap.get(b2);
                				value = new Integer(value.intValue()+1);
                				resultMap.put(b2, value);
                				if (generateWarnings) System.err.println("  re-added book "+book.getRecordReference()+" with authors "+book.getContributorWords() + " value is now "+value);
        					}
        	
        				}


        			} else {
        				resultMap.put(book, 0);
        				if (generateWarnings) System.err.println("  added book "+book.getRecordReference()+" with authors "+book.getContributorWords() );

        			}
        		}
        	}
    	}
    	if (generateWarnings) System.err.println("resultmap now has "+resultMap.size()+ " entries");
    	return sortHashMap(resultMap, true);
    }
    
    public List<PhysicalBookManifestation> getBooksByTitle(String title) {
    	boolean generateWarnings = false;
    	HashSet<Long> pbmSet1 = new HashSet<Long>();
    	HashSet<Long> pbmSet2 = new HashSet<Long>();
    	HashMap<PhysicalBookManifestation, Integer> resultMap = new HashMap<PhysicalBookManifestation, Integer>();
    	/*
    	 * retrieve full title matches first. At present, this will never yield results because there is no normalized
    	 * title field present in the entities in the datastore. Titles in the datastore start with capital, but titles 
    	 * that are passed into this function are typically all lowercase. So this code is commented out until the 
    	 * normalized title field is added.
    	 */
    	
    	/*
    	List<PhysicalBookManifestation> fullTitleList = getBooksByFullTitle(title);
    	if (generateWarnings) System.err.println("fullTitleList has "+fullTitleList.size()+" items");
    	for (PhysicalBookManifestation book : fullTitleList) {
    		if (generateWarnings) System.err.println("  adding book "+book.getRecordReference()+" with title "+book.getTitle());
    		pbmSet1.add(book.getRecordReference());
    		resultMap.put(book, 99);
    	}
    	if (generateWarnings) System.err.println("resultmap now has "+resultMap.size()+ " entries");
    	 */
    	
    	/*
    	 * retrieve partial title matches: parse title into words (ArrayList titleWords), remove stopwords
    	 * and then look up each word.
    	 */
    	List<String> titleWords = new ArrayList<String>();
    	String[] result = title.toLowerCase().split("[,\\.\\s]");
    	for (int x=0; x<result.length; x++) {
    		if (StopWords.isAStopWord(result[x]) == false && result[x].length()>1) titleWords.add(result[x]);
    	}
    	
    	for (String titleWord : titleWords) {
    		List<PhysicalBookManifestation> booksByWordList = getBooksByTitleWord(titleWord, 0, 0);
    		if (generateWarnings) System.err.println("booksByWordList (word "+titleWord+") has "+booksByWordList.size()+" items");
    		/* put everything in a set to ensure uniqueness*/
        	pbmSet2 = new HashSet<Long>();
        	for (PhysicalBookManifestation book : booksByWordList) {
        		/*
        		 * check for uniqueness in the result set of the last query
        		 */
        		if (pbmSet2.add(book.getRecordReference()) != false) {
        			/*
        			 * check for uniqueness in the final result set.  If this book can't be added to pbmSet1, 
        			 * we know it's already in that set. So retrieve/delete/store it with increased sort priority. 
        			 */
        			if (pbmSet1.add(book.getRecordReference()) == false) {
        				/*
        				 * since this book could not be added to pbmSet1, we know it's already in that set. So retrieve 
        				 * it and increase the sort priority. 
        				 */
        				Set<PhysicalBookManifestation> keys = resultMap.keySet();
        				for (PhysicalBookManifestation b2: keys) {
        					if (b2.getRecordReference() == book.getRecordReference()) {
                				Integer value = (Integer)resultMap.get(b2);
                				value = new Integer(value.intValue()+1);
                				resultMap.put(b2, value);
                				if (generateWarnings) System.err.println("  re-added book "+book.getRecordReference()+" with title "+book.getTitle() + " value is now "+value);
        					}
        	
        				}


        			} else {
        				resultMap.put(book, 0);
        				if (generateWarnings) System.err.println("  added book "+book.getRecordReference()+" with title "+book.getTitle());

        			}
        		}
        	}
    	}
    	if (generateWarnings) System.err.println("resultmap now has "+resultMap.size()+ " entries");
    	return sortHashMap(resultMap, true);
    }
    
    private ArrayList<PhysicalBookManifestation> sortHashMap(HashMap<PhysicalBookManifestation, Integer> input, boolean strictand){
        ArrayList<PhysicalBookManifestation> resultArray = new ArrayList<PhysicalBookManifestation>();
        for (int i=120; i>=0; i--) {
        	if (input.containsValue(i)) {
        		for (PhysicalBookManifestation b: input.keySet()) {
        			if (input.get(b) == i) {
        				resultArray.add(b);
        			}
        		}
        		if (strictand == true) break;
        	}
        }
        return resultArray;
    }
        
    
    public List<PhysicalBookManifestation> getBooksByFullTitle(String title) {
    	registerServices();
    	Objectify ofy = ObjectifyService.begin();
    	/*List<PhysicalBookManifestation> pbmList = new ArrayList<PhysicalBookManifestation>();
    	pbmList = (List<PhysicalBookManifestation>)ofy.query(PhysicalBookManifestation.class).filter("title =", title).list();
    	*/
    	System.err.println("getBooksByFullTitle("+title+")");
    	String start = title.toLowerCase();
    	List<PhysicalBookManifestation> pbmList = (List<PhysicalBookManifestation>)ofy.query(PhysicalBookManifestation.class).filter("title >=", start).filter("title <", start + "\uFFFD").limit(500).list();
    	System.err.println("getBooksByFullTitle("+title+") - returned from query");

    	/*System.err.println("getting all the books");
    	List<PhysicalBookManifestation> pbmList = getAllPhysicalBookManifestations();
    	List<PhysicalBookManifestation> resultpbmList = getAllPhysicalBookManifestations();
    	System.err.println("iterating through all the books ("+pbmList.size()+") total");
    	for (PhysicalBookManifestation b : pbmList) {
    		if (b.getTitle().contains(title)) {
    			resultpbmList.add(b);
    		}
    	}
    	
    	pbmList = new ArrayList<PhysicalBookManifestation>();*/
    	return pbmList;
    }
    
    public List<PhysicalBookManifestation> getBooksByTitleWord(String title, int offset, int limit) {
    	registerServices();
    	//Objectify ofy = ObjectifyService.begin();
    	/*List<PhysicalBookManifestation> pbmList = new ArrayList<PhysicalBookManifestation>();
    	pbmList = (List<PhysicalBookManifestation>)ofy.query(PhysicalBookManifestation.class).filter("title =", title).list();
    	*/
    	int queryLimit = 0;
    	System.err.println("getBooksByTitleWords("+title+")");
    	String start = title.toLowerCase();
    	List<PhysicalBookManifestation> pbmList = (List<PhysicalBookManifestation>)ob.query(PhysicalBookManifestation.class).filter("titleWords >=", start).filter("titleWords <", start + "\uFFFD").limit(limit).list();
    	System.err.println("getBooksByTitleWords("+title+") - returned from query");

    	/*System.err.println("getting all the books");
    	List<PhysicalBookManifestation> pbmList = getAllPhysicalBookManifestations();
    	List<PhysicalBookManifestation> resultpbmList = getAllPhysicalBookManifestations();
    	System.err.println("iterating through all the books ("+pbmList.size()+") total");
    	for (PhysicalBookManifestation b : pbmList) {
    		if (b.getTitle().contains(title)) {
    			resultpbmList.add(b);
    		}
    	}
    	
    	pbmList = new ArrayList<PhysicalBookManifestation>();*/
    	return pbmList;
    }
  
    public HashSet<Key<PhysicalBookManifestation>> getBookKeysByISBN(String isbn, int offset, int limit) {
    	MyQuery<PhysicalBookManifestation> query = new MyQuery<PhysicalBookManifestation>(ob.query(PhysicalBookManifestation.class), offset, limit);
    	List<Key<PhysicalBookManifestation>> list = new ArrayList<Key<PhysicalBookManifestation>>();
    	if (isbn.length() == 13) {
    		list = query
    				.filter("recordReference =", new Long(isbn).longValue())
    				.listKeys();
    	} else {
       		String padding = "00000000000000000000";
        	String startIsbn = isbn + padding.substring(0, 13-isbn.length());
        	padding = "9999999999999999";
        	String endIsbn = isbn + padding.substring(0, 13-isbn.length());
    		list = query
    				.filter("recordReference >=", new Long(startIsbn).longValue())
    				.filter("recordReference <=", new Long(endIsbn).longValue())
    				.listKeys();    		
    	}
    	
    	HashSet<Key<PhysicalBookManifestation>> keys = new HashSet<Key<PhysicalBookManifestation>>();
    	keys.addAll(list);
    	return keys;   	      	
    	
    }
    
    
    public List<PhysicalBookManifestation> getBooksByISBN(String isbn, int offset, int limit) {
    	HashSet<Key<PhysicalBookManifestation>> isbnKeys = getBookKeysByISBN(isbn, offset, limit);
    	Map<Key<PhysicalBookManifestation>, PhysicalBookManifestation> books = getBooksByKeysInList(isbnKeys);
    	List<PhysicalBookManifestation> list = new ArrayList<PhysicalBookManifestation>();
    	list.addAll(books.values());
    	return list;
    }

    public List<PhysicalBookManifestation> getBooksByISBN(String isbn) {
    	return getBooksByISBN(isbn, 0, 0);
    }

    public List<PhysicalBookManifestation> getBooksByRecordReference(String isbn) {
    	return getBooksByISBN(isbn);
    }
    
    public List<PhysicalBookManifestation> getBooksByNameWord(String authorname) {
    	System.err.println("getBooksByNameWord("+authorname+")");

    	registerServices();
    	Objectify ofy = ObjectifyService.begin();
    	String start = authorname;

    	List<PhysicalBookManifestation> pbmList = new ArrayList<PhysicalBookManifestation>();
    	/* first find the author in the entity database */
    	/*Person p = getPersonByFullName(authorname);
    	if (p == null) {
    		System.err.println("Author not found, returning ");
    		return pbmList;
    	}*/
    	pbmList = (List<PhysicalBookManifestation>)ofy.query(PhysicalBookManifestation.class).filter("contributorWords >=", start).filter("contributorWords <", start + "\uFFFD").limit(500).list();
    	//pbmList = (List<PhysicalBookManifestation>)ofy.query(PhysicalBookManifestation.class).filter("contributor.nameOnBook =", start).limit(100).list();
       	return pbmList;
    }
    
    public List<Key<PhysicalBookManifestation>> getBookKeysByRecordReferenceNewerThan(Long recordReference, Date d) {
    	return this.getBookKeysByRecordReferenceRelativeToDate(recordReference, d, ">");
    }
    
    public List<Key<PhysicalBookManifestation>> getBookKeysByRecordReferenceRelativeToDate(Long recordReference, Date d, String comparison) {
		List<Key<PhysicalBookManifestation>> list = new ArrayList<Key<PhysicalBookManifestation>>();
    	if (comparison.equals(">") || comparison.equals("<") || comparison.equals("<=") || comparison.equals(">=") || comparison.equals("=")) {
    		registerServices();
    		Objectify ofy = ObjectifyService.begin();
    		String filterString = "firstModificationDate "+ comparison+" ";
    		list = ofy.query(PhysicalBookManifestation.class).filter("recordReference = ", recordReference).filter(filterString, d).listKeys();
    	}
    	return list;
    }

    public List<Key<PhysicalBookManifestation>> getBookKeysByRecordReferenceOlderThan(Long recordReference, Date d) {
    	return this.getBookKeysByRecordReferenceRelativeToDate(recordReference, d, "<");
    }

    public List<Key<PhysicalBookManifestation>> getBookKeysByRecordReference(Long recordReference) {
    	return getBookKeysByRecordReference(recordReference, 0, -1, true);
    }
    
    public HashSet<Key<PhysicalBookManifestation>> getBookKeysByTitle(String title) {
    	return getBookKeysByTitle(title, 0, -1, true);
    }
    
    public boolean doesBookExistByRecordReference(Long recordReference) {
    	List<Key<PhysicalBookManifestation>> list = this.getBookKeysByRecordReference(recordReference);
    	if (list.size() > 0) return true;
    	return false;
    }
    
    /*
     * arg offset: offset
     * arg limit: taken into account if 0 or higher, ignored if -1 (default is then used). Default limit is 500.
     */
    public List<Key<PhysicalBookManifestation>> getBookKeysByRecordReference(Long recordReference, long offset, int limit, boolean sortAscending) {
    	int queryLimit = 500;
    	List<Key<PhysicalBookManifestation>> list = new ArrayList<Key<PhysicalBookManifestation>>();
    	registerServices();
    	Objectify ofy = ObjectifyService.begin();
    	if (limit >= 0) queryLimit = limit; 
        list = ofy.query(PhysicalBookManifestation.class).filter("recordReference = ", recordReference).limit(queryLimit).listKeys();
    	return list;	
    }
    
    /*
     * arg offset: offset
     * arg limit: taken into account if 0 or higher, ignored if -1 (default is then used). Default limit is 500.
     */
    public HashSet<Key<PhysicalBookManifestation>> getBookKeysByTitle(String title, long offset, int limit, boolean sortAscending) {
    	int queryLimit = 500;
    	boolean generateWarnings = false;
    	List<PhysicalBookManifestation> list = getBooksByTitle(title);
    	HashSet<Key<PhysicalBookManifestation>> keys = new HashSet<Key<PhysicalBookManifestation>>();
    	for (PhysicalBookManifestation pbm : list) {
    		keys.add(new Key<PhysicalBookManifestation>(PhysicalBookManifestation.class, pbm.getId()));
    	}
    	return keys;
    }
    
   
    public HashSet<Key<PhysicalBookManifestation>> getBookKeysByNurcode(int nurcode, int offset, int limit, boolean sortAscending) {
    	return queryBookKeysByContentSpecifier(ContentSpecifierType.NURCODE, nurcode, offset, limit, sortAscending);
    }
    
    public HashSet<Key<PhysicalBookManifestation>> getBookKeysByNurcode(int lower, int upper, int offset, int limit, boolean sortAscending) {
    	return queryBookKeysByContentSpecifier(ContentSpecifierType.NURCODE, lower, upper, offset, limit, sortAscending);
    }
    
    public HashSet<Key<PhysicalBookManifestation>> getBookKeysByGeocode(String geocode, int offset, int limit, boolean sortAscending) {
    	return queryBookKeysByContentSpecifier(ContentSpecifierType.GEOCODE, geocode, offset, limit, sortAscending);
    }
    
    private HashSet<Key<PhysicalBookManifestation>> queryBookKeysByContentSpecifier(ContentSpecifierType contentSpecifierType, int code, int offset, int limit, boolean sortAscending) {
    	MyQuery<PhysicalBookManifestation> query = new MyQuery<PhysicalBookManifestation>(ob.query(PhysicalBookManifestation.class), offset, limit);
    	List<Key<PhysicalBookManifestation>> list = new ArrayList<Key<PhysicalBookManifestation>>();
    	list = query
    	.filter("contentSpecifiers.code =", code)
    	.filter("contentSpecifiers.contentSpecifierType = ", contentSpecifierType)
    	.listKeys();
    	
    	HashSet<Key<PhysicalBookManifestation>> keys = new HashSet<Key<PhysicalBookManifestation>>();
    	keys.addAll(list);
    	return keys;   	      	
    }
    
    public HashSet<Key<PhysicalBookManifestation>> queryBookKeysByContentSpecifier(ContentSpecifierType contentSpecifierType, int lower, int upper, int offset, int limit, boolean sortAscending) {
    	MyQuery<PhysicalBookManifestation> query = new MyQuery<PhysicalBookManifestation>(ob.query(PhysicalBookManifestation.class), offset, limit);
    	List<Key<PhysicalBookManifestation>> list = new ArrayList<Key<PhysicalBookManifestation>>();
    	list = query
    	.filter("contentSpecifiers.code >=", lower).filter("contentSpecifiers.code <=", upper)
    	.filter("contentSpecifiers.contentSpecifierType = ", contentSpecifierType)
    	.listKeys();
    	
    	HashSet<Key<PhysicalBookManifestation>> keys = new HashSet<Key<PhysicalBookManifestation>>();
    	keys.addAll(list);
    	return keys;   	
    }
    
    public HashSet<Key<PhysicalBookManifestation>> queryBookKeysByContentSpecifier(ContentSpecifierType contentSpecifierType, String description, int offset, int limit, boolean sortAscending) {
    	MyQuery<PhysicalBookManifestation> query = new MyQuery<PhysicalBookManifestation>(ob.query(PhysicalBookManifestation.class), offset, limit);
    	List<Key<PhysicalBookManifestation>> list = new ArrayList<Key<PhysicalBookManifestation>>();
    	list = query
    	.filter("contentSpecifiers.description =", description)
    	.filter("contentSpecifiers.contentSpecifierType = ", contentSpecifierType)
    	.listKeys();
    	HashSet<Key<PhysicalBookManifestation>> keys = new HashSet<Key<PhysicalBookManifestation>>();
    	keys.addAll(list);
    	return keys;
    }

	public HashSet<Key<PhysicalBookManifestation>> getBookKeysByContributor(
			String value, String flags) {
    	List<PhysicalBookManifestation> list = getBooksByAuthor(value);
    	HashSet<Key<PhysicalBookManifestation>> keys = new HashSet<Key<PhysicalBookManifestation>>();
    	for (PhysicalBookManifestation pbm : list) {
    		keys.add(new Key<PhysicalBookManifestation>(PhysicalBookManifestation.class, pbm.getId()));
    	}
    	return keys;
	}

	public Map<Key<PhysicalBookManifestation>, PhysicalBookManifestation> getBooksByKeysInList(
			HashSet<Key<PhysicalBookManifestation>> recordReferences) {
    	boolean generateWarnings = true;
		Map<Key<PhysicalBookManifestation>, PhysicalBookManifestation> books = ob.get(recordReferences);
		for (PhysicalBookManifestation book : books.values()) {
			/*
			 * decorate with images, if available
			 */
			try {
				for (String urlString : getUrlsForImageReference(book.getRecordReference(), ImageReferenceTypeCode.FRONTCOVER)) {
					if (urlString != null) {
						ImageReference imageReference = new ImageReference();
						imageReference.setImageReferenceTypeCode(ImageReferenceTypeCode.FRONTCOVER);
						imageReference.setUrl(urlString);
						book.getImageReferences().add(imageReference);
					}
				}
				for (String urlString : getUrlsForImageReference(book.getRecordReference(), ImageReferenceTypeCode.BACKCOVER)) {
					if (urlString != null) {
						ImageReference imageReference = new ImageReference();
						imageReference.setImageReferenceTypeCode(ImageReferenceTypeCode.BACKCOVER);
						imageReference.setUrl(urlString);
						book.getImageReferences().add(imageReference);
					}
				}
			} catch (Exception e) {
				System.err.println("Exception thrown in PersistentServiceImpl / getBooksByKeysInList(): ");
				e.printStackTrace();
			}
			/*
			 * retrieve contributor info
			 */
    		for (Contributor c : book.getContributors()) {
    			if (generateWarnings) {
    				System.err.println("retrieving person: "+c.getNameOnBook());
    				if (c.getPerson() == null) {
    					System.err.println("  person is null");
    				} else {
    					System.err.println("  person is not null");
    				}
    			}
    			Key<Person> personKey = c.getPersonKey();
    			if (personKey != null) {
    				Person p = getPersonByKey(personKey);
    				if (p == null) {
    					if (generateWarnings) System.err.println("person was not part of book, manual lookup");
    					p = getPersonByFullName(c.getNameOnBook());
    				}
    				c.setPerson(p);
    			}
    		}
    	}
    	return books;
	}
	
	public PhysicalBookManifestation getBookByKey (Key<PhysicalBookManifestation> key) {
		HashSet<Key<PhysicalBookManifestation>> keyList = new HashSet<Key<PhysicalBookManifestation>>();
		keyList.add(key);
		Map<Key<PhysicalBookManifestation>, PhysicalBookManifestation> returnMap = getBooksByKeysInList(keyList);
		return returnMap.get(key);
	}
 
}
